package server;

import java.util.*;

import server.data.*;

import dictionary.data.LocationIndex;
import dictionary.gameEntities.*;
import dictionary.interfaceData.*;
import dictionary.interfaceData.NetworkDataWrapper.ActionType;

/**
 * Overall status - finished, but needs testing and debugging
 * 
 * @author mjp
 * @version 1.0
 * @created 06-Nov-2011 10:34:55 AM
 */
public class Server {

	
	static final boolean DEBUG = false;

	public void out(String text)
	{
		if (DEBUG)
		{
			System.out.println("Server ("+this.toString()+"): "+text);
		}
	}
	
	private static final String MOVE_PROMPT = "Please select room to move to.";

	private static final String ACCUSE_PROMPT = "Please make an accusation or pass.";;

	private static boolean TEST_MODE = false;

	// public only for testing
	public List<Player> allPlayers;
	
	private GameState latestGameState;

	private SuggestOrAccuseData suggestion;
	
	private CardIndex cardIndex;
	
	private ServerMessager messager;

	HashMap <Player, ArrayList<StatusLogEntry>> statusLogEntriesByPlayer = 
			new HashMap<Player, ArrayList<StatusLogEntry>>();

	private List<Player> orderedPlayerList = new ArrayList<Player>();

	// messages that appear in the event log have an incrementing messageId
	private int runningMessageId = 0;

	private ArrayList<Player> losers = new ArrayList<Player>();

	private NetworkDataWrapper testerUpdateData;

	private Player dummyPlayer = new Player(100);
	
	public static void main(String[] args) {
		new Server();
	}
	
	public Server() {
		this(false);
	}
	
	public Server(boolean inTesting) {
		TEST_MODE = inTesting;
		dummyPlayer.character = CharacterIndex.getCharacters().get(0);
		startGame();
	}
	
	/**
	 * finished
	 * When the window for accepting new users has closed, start the game
	 * @param inTestMode 
	 */
	protected void startGame()
	{
		if (! TEST_MODE)
		{
			try {
				out("Creating messager");
				messager = new ServerMessager(this);
				out("Messager created");
			}
	        catch(Exception e) {
	           out("Error creating new ServerMessager: " + e); 
	           e.printStackTrace();
	        }
			try {
				out("Starting to establish clients");
				allPlayers = messager.establishClientConnections();
				out("all clients received");
			}
	        catch(Exception e) {
	           out("Error establishing client connections: " + e); 
	           e.printStackTrace();
			}
		}
		else // in TEST MODE 
		{
			allPlayers = testerGenerateDummyPlayers();
			messager = new TesterDummyMessager();
			System.out.println(allPlayers.toString());
		}
		
		try {
			// all players are in. dealCards and assignCharacters
			dealCardsToPlayers();
			out("assigning Players");
			assignCharactersToPlayers();
			out("finished assigning Players");
		} catch (Exception e) {
			out("Error dealing cards and assigning characters to players: " + e);
			e.printStackTrace();
		}
		try {
			// create the initial GameState, setting current turn
			// to player first with pre-defined ordering
			this.latestGameState = createInitialGameState();
		} catch (Exception e) {
			out("Error creating initial game state: " + e);
			e.printStackTrace();
		}
		try {
			// wrap the GameState
			NetworkDataWrapper wrapper = new NetworkDataWrapper();
			
			wrapper.gameState = this.latestGameState;

			wrapper.currentAction = NetworkDataWrapper.ActionType.INIT_PLAYER;

			wrapper.statusLogEntries = statusLogEntriesByPlayer;

			// update wrapper with each player's data object and message log,
			// then send out to each Client
			for (Player currPlayer : allPlayers) {
				ArrayList<StatusLogEntry> messageList = new ArrayList<StatusLogEntry>();
				messageList.add(new StatusLogEntry("The game begins.", new Date(
						System.currentTimeMillis()), runningMessageId++));
				out("sent player data: " + currPlayer.toString());
				wrapper.playerData = currPlayer;
				// same initial status log for all
				statusLogEntriesByPlayer.put(currPlayer, messageList);
				messager.sendData(wrapper, currPlayer);
				if (TEST_MODE) testerUpdateData= wrapper;
			}
		} catch (Exception e) {
			out("Error updating wrapper with each player's data object and message log: "
					+ e);
			e.printStackTrace();
        }
	}
	
	private List<Player> testerGenerateDummyPlayers() {
		ArrayList<Player> players = new ArrayList<Player>();
		List<ClueCharacter> characters = CharacterIndex.getCharacters();
		for (int i = 0; i<6; i++)
		{
			Player curr = new Player(i);
			curr.character = characters.get(i);
			players.add(curr);
			
		}
		return players;
	}

	/**
	 * finished
	 * 
	 * only public for testing
	 */
	public GameState createInitialGameState() {
		Player currPlayer = orderedPlayerList.get(0);
		
		String turnStatus = "Awaiting move from "
				+ currPlayer.character.getName();
		
		// the first game state always has the following conditions:
		// next action is a player move
		// the default character positions from the character index are valid
		// the default weapon positions from the weapon index are valid
		GameState state = new GameState(currPlayer,
				UserActionTypes.PLAYER_MOVE, CharacterIndex.getCharacters(),
				WeaponIndex.getWeapons(), MOVE_PROMPT,
				turnStatus);
		
		return state;
	}

	/**
	 * finished
	 * Randomly assigns a character to every player
	 */
	private void assignCharactersToPlayers()
	{
		// don't shuffle the original list of characters because their 
		// ordering indicates player turn.
		// instead make a copy and shuffle the copy, then assign to players
		ArrayList<ClueCharacter> characters = new ArrayList<ClueCharacter>();
		characters.addAll(CharacterIndex.getCharacters());
		Collections.shuffle(characters, new Random(System.currentTimeMillis()));
		Iterator<ClueCharacter> characterIter = characters.iterator();
		for (Player currPlayer : allPlayers)
		{
			// assumes that we always have enough characters
			currPlayer.character = characterIter.next();
		}
		
		// create an ordered list of the players, 
		// ordered by character id's which is established by CharacterIndex
		orderedPlayerList.addAll(allPlayers);
		Collections.sort(orderedPlayerList, new Comparator<Player>() {

			@Override
			public int compare(Player o1, Player o2) {
				return o1.character.getId() - o2.character.getId();
			}
		});
	}

	/**
	 * finished
	 * Randomly provides all non-case-file cards to players
	 */
	private void dealCardsToPlayers() 
	{
		cardIndex = new CardIndex();
	
		int playerIndex = 0;
		
		List<Card> deck = cardIndex.getCards();

		// initialize the player's hands
		for (Player currPlayer : allPlayers)
		{
			currPlayer.playerCards = new Hand();
		}
		
		// iterate through cards, passing each to next player's hand (in order) until finished 
		for (Card currCard : deck)
		{
			allPlayers.get(playerIndex).playerCards.addCard(currCard);
			playerIndex = (playerIndex + 1 ) % allPlayers.size();
		}
	}

	/**
	 * finished
	 * Based on the data received, make the appropriate change
	 * @param data
	 */
	public void receiveDataFromClient(NetworkDataWrapper data)
	{
		// always going to be GameStateChange from client
		if ( ! data.currentAction.equals(ActionType.USER_ACTION))
		{
			throw new RuntimeException("Invalid Client to Server message type");
		}

		// look at type - switch statement
		switch(data.currentUserAction)
		{
			case ACCUSE:
				this.handleAccusation(data.suggestionOrAccusation);
				break;
			case DISPROVE:
				this.handleDisprove(data.disproval);
				break;
			case PLAYER_MOVE:
				handleMove(data.moveData);
				break;
			case SUGGEST:
				handleSuggest(data.suggestionOrAccusation);
				break;
		}
	}
	
	/**
	 * finished
	 * assumes that move is valid.
	 * Doesn't implement "stay"  
	 */
	public void handleMove(PlayerMoveData data)
	{
		out("handling Player Move");
		out("Player "+data.getMovingPlayer().character.getName()+" moved to "+data.getNewLocation().getName());
		NetworkDataWrapper message = new NetworkDataWrapper();
		GameState update = createNewGameState(data);

		update.currentPlayer = data.getMovingPlayer();
		
		// handle stay
		update.currentPlayer.character.setMovedBySuggestion(false);
		int prevLocation = -1;
		for (ClueCharacter c : this.latestGameState.charactersPositions)
		{
			if (c.getId() == data.getMovingPlayer().character.getId())
			{
				prevLocation = c.getLocation().getId();
			}
		}
		if (data.getNewLocation().getId() == prevLocation)
		{
			addStatusLogEntryForAll(data.getMovingPlayer().character.getName()+
					" stayed in the\n"+
					data.getNewLocation().getName());
		}
		else 
		{
			// add status Log entry for everyone
			addStatusLogEntryForAll(data.getMovingPlayer().character.getName()+
					" moved to\n"+
					data.getNewLocation().getName());
		}
		// end handle stay
		
		// determine if the move was to a hallway
		if (data.getNewLocation().getClass() == Hallway.class) 
		{
			// current Player gets a chance to accuse
			Player nextPlayer = this.latestGameState.currentPlayer;
			update.pendingAction = UserActionTypes.ACCUSE;
			update.currentPlayer = nextPlayer;
			update.nextActionText = ACCUSE_PROMPT;
			update.playerTurnStatus = "Awaiting accusation from "+
					nextPlayer.character.getName()+".";
		}
		else
		{
			// keep current player as-is
			update.pendingAction = UserActionTypes.SUGGEST;
			update.nextActionText = "Please make a suggestion.";
			update.playerTurnStatus = "Awaiting suggestion from "+
					update.currentPlayer.character.getName()+".";
		}

		// send the game state to everyone
		notifyClients(message, update);
		out("New game state: ");
		out(message.toString());
	}
	

	/**
	 * finished
	 * @param entryText adds the entryText to all player's logs
	 */
	private void addStatusLogEntryForAll(String entryText) {
		StatusLogEntry newEntry = new StatusLogEntry(entryText,
				new Date(System.currentTimeMillis()), this.runningMessageId++);
		for (List<StatusLogEntry> currList : this.statusLogEntriesByPlayer
				.values()) {
			currList.add(newEntry);
		}
	}

	/**
	 * finished
	 * Creates a new game state, using the last distributed game state, except
	 * for a change in the player location due to player move data
	 */
	private GameState createNewGameState(PlayerMoveData data) {
		GameState update = new GameState(this.latestGameState);
		out(data.getNewLocation().toString());
		out(data.getMovingPlayer().toString());
		movePlayerWithinGameState(data.getMovingPlayer().character, update, data.getNewLocation());
		return update;
	}
	
	private void movePlayerWithinGameState(ClueCharacter characterIn, GameState updateIn, Location loc)
	{
		characterIn.setLocation(loc);
		int i = 0;
		for (ClueCharacter character : updateIn.charactersPositions)
		{
			if (characterIn.getId() == character.getId())
			{
				updateIn.charactersPositions.set(i, new ClueCharacter(characterIn));
			}
			i++;
		}
	}
	/**
	 * finished
	 * Sends updated game state to all players,
	 * adding the status logs and the action type.
	 * Saves the game state for later.
	 * @param update
	 */
	private void notifyClients(NetworkDataWrapper message, GameState update) 
	{
		if (! TEST_MODE)
		{
			message.gameState = update;
			// save game state for later
			this.latestGameState = update;
			
			message.currentAction = ActionType.GAME_STATE_CHANGE;

			// do a deeper copy of the map of status messages - 
			// this ensures that serialized map is treated as changed 
			message.statusLogEntries =
					new HashMap<Player, ArrayList<StatusLogEntry>>();
			for (Player curr : this.statusLogEntriesByPlayer.keySet())
			{
				ArrayList<StatusLogEntry> temp = new ArrayList<StatusLogEntry>();
				temp.addAll(this.statusLogEntriesByPlayer.get(curr));
				Collections.sort(temp);
				message.statusLogEntries.put(curr, temp);
			}
			
			this.messager.sendDataToAll(message);
		}
		else testerUpdateData = message;
	}

	public NetworkDataWrapper testerGetUpdate()
	{
		return testerUpdateData;
	}
	/**
	 * finished
	 */
	private GameState createAccuseGameState(Player player) 
	{
		GameState updatedState = new GameState(this.latestGameState);
		updatedState.currentPlayer = player;
		updatedState.nextActionText = ACCUSE_PROMPT;
		updatedState.playerTurnStatus = "Awaiting move from "+player.character.getName();
		updatedState.pendingAction = UserActionTypes.ACCUSE;
		return updatedState;
	}

	/**
	 * finished
	 */
	private Player determinePlayerTurn(Player playerIn) {
		int index = this.orderedPlayerList.indexOf(playerIn);
		Player result = this.orderedPlayerList.get((index+1)%orderedPlayerList.size());
		// skip losers that made incorrect accusations
		if (losers.contains(result))
		{
			result = determinePlayerTurn(result);
		}
		return result;
	}

	/**
	 * finished
	 * assumes valid disproval
	 */
	public void handleDisprove(DisproveData data)
	{
		Player disprover = data.getDisprovingPlayer();
		Player suggester = this.suggestion.getSuggestingPlayer();
		
		// we don't need to validate the disproval because users can 
		// only disproved if allowed by GUI
		
		// sends data to each user as needed
		createDisprovalMessages(suggester, disprover, data.getDisprovalCard());

		// the next player will be the stored suggester who is being disproved
		Player nextPlayer = suggestion.getSuggestingPlayer();		
		
		// create the new game state object and send the data to all clients
		NetworkDataWrapper message = new NetworkDataWrapper();
		notifyClients(message, createAccuseGameState(nextPlayer));
		
		// suggestion data shouldn't be saved any more
		suggestion = null;
	}
	
	/**
	 * finished
	 */
	private void createDisprovalMessages(Player suggester, Player disprover, 
			Card disprovalCard) 
	{
		// create messages for each client.  
		// Note that disprover and suggester get one message, while 
		// others get a different one
		
		// register special messages for uninvolved players
		String uninvolved = 
				createMessageTextForNotInvolved(suggester, disprover);
		StatusLogEntry uninvolvedEntry = new StatusLogEntry(uninvolved, 
				new Date(System.currentTimeMillis()), this.runningMessageId++);
		for (Player curr : allPlayers)
		{
			if (!curr.equals(disprover) && !curr.equals(suggester))
			{
				statusLogEntriesByPlayer.get(curr).add(uninvolvedEntry);
			}
		}

		// register special messages for involved players
		String involved = 
				createMessageTextForInvolved(suggester, disprover, disprovalCard);
		StatusLogEntry involvedEntry = new StatusLogEntry(involved, 
				new Date(System.currentTimeMillis()), this.runningMessageId);
		statusLogEntriesByPlayer.get(disprover).add(involvedEntry);
		statusLogEntriesByPlayer.get(suggester).add(involvedEntry);
	}

	/**
	 * finished
	 */
	private String createMessageTextForInvolved(Player suggester, 
			Player disprover, Card disprovalCard) 
	{
		return createMessageTextForNotInvolved(suggester, disprover)
				+" with the "+
				disprovalCard.getCardName();
	}

	/**
	 * finished
	 */
	private String createMessageTextForNotInvolved(Player suggester,
			Player disprover) 
	{
		return disprover.character.getName()+
				" disproved "+
				suggester.character.getName()+
				"'s suggestion";
	}

	/**
	 * finished
	 */
	public void handleAccusation(SuggestOrAccuseData data)
	{
		// the player could choose not to accuse
		if (data.isPass())
		{
			addStatusLogEntryForAll(data.getSuggestingPlayer().character.getName()+" chose not to\n make an accusation.");

			// send a game state, giving next player chance to move
			GameState update = new GameState(this.latestGameState);
			update.currentPlayer = determinePlayerTurn(data.getSuggestingPlayer());
			update.nextActionText = MOVE_PROMPT;
			update.pendingAction = UserActionTypes.PLAYER_MOVE;
			update.playerTurnStatus = "Awaiting move from "+update.currentPlayer.character.getName();
			NetworkDataWrapper message = new NetworkDataWrapper();
			notifyClients(message, update);
		}
		// if an accusation took place
		else
		{
			// verify accusation against case file
			if (cardIndex.caseFile.isMatch(data.getWeapon(), data.getSuspect(), data.getRoom()))
			{
				out("end game!!!!");
				// end the game.  the suggesting player is the winner
				endGame(data.getSuggestingPlayer(), " wins with a correct accusation!");
			}
			else
			{
				// if incorrect, 
				// - inform accusing user of case file contents via  
				// event log message to only that user
				this.statusLogEntriesByPlayer.get(data.getSuggestingPlayer()).add(
						new StatusLogEntry("Incorrect accusation. Case file: "+
								cardIndex.caseFile.toString(), 
								new Date(System.currentTimeMillis()), this.runningMessageId++));
				
				// - inform other players that this player made an incorrect accusation
				addStatusLogEntryForAll(data.getSuggestingPlayer().character.getName()+" loses \nwith an incorrect accusation.");
				
				// - add the accusing player to the list of losers, meaning that this 
				// user can never again move, suggest or accuse.  They can still disprove.
				losers.add(data.getSuggestingPlayer());
				
				out("------CHECK for end of game--------");
				out(losers.toString());
				out(orderedPlayerList.toString());
				// if bad accusation means all but one player lost, end game.
				if (losers.size() == (orderedPlayerList.size() - 1))
				{
					// determine winner
					Player winner = null;
					for (Player curr : orderedPlayerList)
					{
						if (! losers.contains(curr)) winner = curr;
						out("wtf: "+ winner);
					}
					// end the game
					out("wtf");
					endGame(winner, " wins by default - all other players lost!");
					out("wtf");
				}
				else
				{
					// send a game state out that shows no active player
					GameState update = new GameState(this.latestGameState);
					update.currentPlayer = determinePlayerTurn(data.getSuggestingPlayer());
					update.nextActionText = MOVE_PROMPT;
					update.pendingAction = UserActionTypes.PLAYER_MOVE;
					update.playerTurnStatus = "Awaiting move from "+update.currentPlayer.character.getName();
					NetworkDataWrapper message = new NetworkDataWrapper();
					notifyClients(message, update);
				}
			}
		}
	}

	/**
	 * finished
	 */
	private void endGame(Player winner, String text) {
		// if correct, update the event log to say who won
		addStatusLogEntryForAll(winner.character.getName()+ "\n" + text);
		// send a game state out that shows no active player
		GameState update = new GameState(this.latestGameState);
		update.currentPlayer = dummyPlayer ;
		update.nextActionText = "";
		update.playerTurnStatus = "Game over.";
		NetworkDataWrapper message = new NetworkDataWrapper();
		message.currentAction = ActionType.GAME_STATE_CHANGE;
		message.currentUserAction = UserActionTypes.PLAYER_MOVE;
		message.playerData = null;
		notifyClients(message, update);

	}

	/**
	 * finished
	 */
	public void handleSuggest(SuggestOrAccuseData data)
	{
		// save this data for later
		this.suggestion = data;
		GameState updatedState = new GameState(this.latestGameState);
		Player disproverFound = null;
		
		// update the event log, with suggestion notification
		String logMessage = data.getSuggestingPlayer().character.getName()+
								" suggests:\n"+
								data.getSuspect().getName()+
								" in the "+
								data.getRoom().getName()+
								"\n with the "+
								data.getWeapon().getName();
		out(logMessage);
		addStatusLogEntryForAll(logMessage);
		// change the location of the implicated character
		
		data.getSuspect().setMovedBySuggestion(true);
		// move the object
		movePlayerWithinGameState(data.getSuspect(), updatedState, 
				LocationIndex.getLocationByName(data.getRoom().getName()));
		
		int j = 0;
		for (Player p : orderedPlayerList)
		{
			if (p.character.equals(data.getSuspect()))
			{
				Player p2 = new Player(p.playerId);
				// changes the location
				p2.character = data.getSuspect();
				p2.playerCards = p.playerCards;
				orderedPlayerList.set(j, p2);
			}
			j++;
		}
		
		// change the location of the implicated weapon
		Weapon weapon = data.getWeapon();
		weapon.setLocation(data.getRoom());
		
		// start looking at the next player, through the last player
		int startIndex = orderedPlayerList.indexOf(data.getSuggestingPlayer());
		// - look at all the player's hands, in player order
		for (int i = 1; null == disproverFound && i < this.allPlayers.size(); i++)
		{
			Player possibleDisprover = 
					orderedPlayerList.get((i+startIndex) % orderedPlayerList.size());
			if (possibleDisprover.playerId != this.suggestion.getSuggestingPlayer().playerId)
			{
				// - determine if player can disprove this suggestion
				if (possibleDisprover.playerCards.findCard(data.getRoom().getName()) ||
					possibleDisprover.playerCards.findCard(data.getSuspect().getName()) ||
					possibleDisprover.playerCards.findCard(data.getWeapon().getName()))
				{
					disproverFound = possibleDisprover;
				}
			}
		}

		NetworkDataWrapper message = new NetworkDataWrapper();
		

		// a suggester may have only selected cards within their own hand or 
		// within the case file
		if (null == disproverFound)
		{
			this.addStatusLogEntryForAll(
					"No players could disprove\n the suggestion of\n"+
					data.getSuggestingPlayer().character.getName());
			updatedState.currentPlayer = this.suggestion.getSuggestingPlayer();
			updatedState.pendingAction = UserActionTypes.ACCUSE;
			updatedState.nextActionText = ACCUSE_PROMPT; 
			updatedState.playerTurnStatus = 
					updatedState.currentPlayer.character.getName()+
					"'s turn\n to make an accusation.";
		}
		// a possible disprover was found 
		else
		{
			// if a user can disprove, create a gamestate that tells the player to disprove
			updatedState.currentPlayer = disproverFound;
			updatedState.pendingAction = UserActionTypes.DISPROVE;
			updatedState.nextActionText = "Please select card for disproval."; 
			updatedState.playerTurnStatus = 
					disproverFound.character.getName()+"'s turn to disprove the suggestion.";
			
			// Send suggestion data to disprover so 
			// that they can only select viable cards
			message.suggestionOrAccusation = data;

			// note: when the disproving player finishes the disprove, 
			// the suggesting player is given their turn back.
		}
		notifyClients(message, updatedState);
	}
}//end Server